from functools import partial, wraps
from typing import List, Set

from flask import current_app
from flask import request as current_request
from flask_restx import abort
from sqlalchemy import and_

from app.common.libs import auth
from app.models.roleModel import (
    METHOD_TO_FILTER,
    METHOD_TO_FLAG,
    FilterChoices,
    Permission,
    PresetRoleChoices,
    RolePermission,
    RolePolicyFilter,
)
from app.models.userModel import User
from configs.settings import ALLOW_NO_LOGIN, RULE_WHITE_LIST, flask_config
from core import ExtRule
from tools import redis_conn


class PermissionControl:
    """权限控制器"""

    def __init__(self, user_policy_filter=flask_config.USER_POLICY_FILTER):
        """
        控制器基本参数
        :param bool user_policy_filter: 策略控制开关
        """
        self.user_policy_filter = user_policy_filter

    @staticmethod
    def login_reset(current_user_name):
        redis_conn.delete(f"LoginUsername_{current_user_name}")

    @staticmethod
    def special_rules(user: User, role: str, rule: ExtRule):
        def vm_enter(_user, _role, _rule):
            """额外允许通过教师接口进入虚拟机的角色"""
            extra_roles = (PresetRoleChoices.rb_referee,)
            return role in extra_roles and rule.rule == "/v1/organizer/vm/enter"

        return any(func(user, role, rule) for func in (vm_enter,))

    def __call__(self, f):
        @wraps(f)
        def decorator(*args, **kwargs):
            rule: ExtRule = current_request.url_rule
            user: User = auth.current_user()
            if not user:
                abort(401, message="无法获取当前用户")
            current_role = current_request.cookies.get("role", None)

            # 动态菜单接口适配
            if (
                not current_role
                and rule.endpoint != "permission_control.menu_view"
                and rule.methods != "POST"
            ):
                self.login_reset(user.username)
                abort(403, message="cookie异常，无法获取cookie信息")
            elif current_role not in PresetRoleChoices:
                self.login_reset(user.username)
                abort(403, message="cookie异常, 角色不存在")

            request_method = current_request.method
            _filter: FilterChoices = METHOD_TO_FILTER[request_method]

            if (
                request_method in ("OPTIONS", "HEAD")
                or rule.rule in ALLOW_NO_LOGIN
                or (user.is_superuser and current_role == PresetRoleChoices.admin)
                or self.special_rules(user, current_role, rule)
            ):
                return current_app.ensure_sync(f)(*args, **kwargs)
            # 角色权限校验 仅校验当前角色
            if current_role:
                user_role_ids: Set[int] = {
                    role.id for role in user.roles if role.name == current_role
                }
                if not user_role_ids:
                    self.login_reset(user.username)
                    abort(403, message="无法获取当前用户分配的角色")
            else:
                user_role_ids: Set[int] = {role.id for role in user.roles}
                if not user_role_ids:
                    abort(403, message="当前用户未分配任何角色")
            role_verify = self.role_permission_validate(
                rule, user_role_ids, request_method
            )
            if self.user_policy_filter:
                # 根据角色权限验证结果，判断用户是否存在针对该rule的disable/allow策略
                strategy_validate = partial(
                    self.open_strategy_validate, user.id, rule, _filter
                )
                strategy_verify = (
                    strategy_validate("disable")
                    if role_verify
                    else strategy_validate("allow")
                )

                if strategy_verify:
                    return current_app.ensure_sync(f)(*args, **kwargs)
            elif role_verify:
                return current_app.ensure_sync(f)(*args, **kwargs)

            abort(403, message="无操作权限")

        return decorator

    @staticmethod
    def role_permission_validate(
        rule: ExtRule, user_role_ids: List[int], request_method: str
    ):
        """角色权限校验"""
        # FIXME: 按靶场当前需求及实际情况，暂时不考虑自定义角色情况
        # 忽略白名单
        if rule.rule in RULE_WHITE_LIST:
            return True

        # 获取用户归属角色拥有的权限
        role_permission_list: RolePermission = RolePermission.query.filter(
            RolePermission.role_id.in_(user_role_ids)
        ).all()
        # 权限ID与权限映射
        permission_map: dict = {
            item.id: item
            for item in Permission.query.filter(
                Permission.id.in_({val.permission_id for val in role_permission_list})
            ).all()
        }
        # 最终获取允许访问的路由及方法
        allow_permission: dict = {
            permission_map[item.permission_id].name: item.flag
            for item in role_permission_list
            if item.flag
        }
        return bool(
            (
                allow_permission.get(rule.endpoint)
                and int(allow_permission[rule.endpoint])
                & METHOD_TO_FLAG[request_method]
            )
        )

    @staticmethod
    def open_strategy_validate(
        user_id: int, rule: ExtRule, _filter: str, strategy: str
    ):
        """开放策略校验"""

        filter_strategy = (
            RolePolicyFilter.query.join(
                Permission,
                and_(
                    RolePolicyFilter.permission_id == Permission.id,
                    Permission.name == rule.endpoint,
                    # 路由策略校验
                    Permission.type == 1,
                    Permission.path == rule.rule,
                ),
            )
            .filter(
                RolePolicyFilter.user_id == user_id,
                RolePolicyFilter.filter == _filter,
                RolePolicyFilter.strategy == strategy,
            )
            .all()
        )

        return False if filter_strategy else True


permission_control = PermissionControl()
